/* 
 * Copyright (c) 2004-2005 Massachusetts Institute of Technology. This code was
 * developed as part of the Haystack (http://haystack.lcs.mit.edu/) research 
 * project at MIT. Permission is hereby granted, free of charge, to any person
 * obtaining a copy of this software and associated documentation files (the 
 * "Software"), to deal in the Software without restriction, including without 
 * limitation the rights to use, copy, modify, merge, publish, distribute, 
 * sublicense, and/or sell copies of the Software, and to permit persons to whom
 * the Software is furnished to do so, subject to the following conditions: 
 * 
 * The above copyright notice and this permission notice shall be included in 
 * all copies or substantial portions of the Software. 
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER  
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE. 
 */
/*
 * Created on Jan 6, 2005
 *
 */
package edu.mit.csail.relo.jdt;

import java.util.StringTokenizer;

import org.apache.log4j.Logger;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IWorkspaceRoot;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.jdt.core.IClassFile;
import org.eclipse.jdt.core.ICompilationUnit;
import org.eclipse.jdt.core.IField;
import org.eclipse.jdt.core.IJavaElement;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.IMember;
import org.eclipse.jdt.core.IMethod;
import org.eclipse.jdt.core.IPackageFragment;
import org.eclipse.jdt.core.IPackageFragmentRoot;
import org.eclipse.jdt.core.IType;
import org.eclipse.jdt.core.JavaCore;
import org.eclipse.jdt.core.JavaModelException;
import org.eclipse.jdt.core.Signature;
import org.eclipse.jdt.core.dom.IBinding;
import org.eclipse.jdt.core.dom.IMethodBinding;
import org.eclipse.jdt.core.dom.IPackageBinding;
import org.eclipse.jdt.core.dom.ITypeBinding;
import org.eclipse.jdt.core.dom.IVariableBinding;
import org.eclipse.jdt.internal.core.SourceRefElement;
import org.openrdf.model.Resource;
import org.openrdf.model.URI;

import edu.mit.csail.relo.ReloCore;
import edu.mit.csail.relo.intro.ReloJavaConnectionUtils;
import edu.mit.csail.relo.modelBridge.DirectedRel;
import edu.mit.csail.relo.store.ReloRdfRepository;

/**
 * @author vineet
 *
 */
public class RJCore {
    static final Logger logger = ReloJDTPlugin.getLogger(RJCore.class);
    
	public static final String PLUGIN_ID = ReloJavaConnectionUtils.ReloJDT_PLUGIN_ID;

	/*
     * Note: properties are not associated with any single model, since there 
     * could be multiple models that they apply to
     */

    private static final URI createReloURI(String str) {
    	return ReloCore.createReloURI(str);
    }

    public static final URI srcResource = createReloURI("jdt#source-resource");
    public static final URI srcStart = createReloURI("jdt#source-start");
    public static final URI srcLength = createReloURI("jdt#source-length");

    
    // global relo-jdt properties
    public static final URI javaType = createReloURI("jdt#javaType");

    // properties
    public static final URI access = createReloURI("jdt#access");
    public static final URI parameter = createReloURI("jdt#parameter");
    public static final URI returnType = createReloURI("jdt#returnType");

    // relations
	public static final URI calls = createReloURI("jdt#calls");
	public static final URI inherits = createReloURI("jdt#inherits");
	public static final URI refType = createReloURI("jdt#refType");
	public static final URI overrides = createReloURI("jdt#overrides");
	
    // object types
	public static final URI classType = createReloURI("jdt#class");
	public static final URI interfaceType = createReloURI("jdt#interface");
	public static final URI fieldType = createReloURI("jdt#field");
	public static final URI methodType = createReloURI("jdt#method");
	public static final URI packageType = createReloURI("jdt#package");
	public static final URI projectType = createReloURI("jdt#project");

	public static final URI publicAccess = createReloURI("jdt#access-public");
	public static final URI protectedAccess = createReloURI("jdt#access-protected");
	public static final URI privateAccess = createReloURI("jdt#access-private");
	public static final URI noAccess = createReloURI("jdt#access-none");

    // support for directory based package exploration
    public static final URI pckgDirType = createReloURI("jdt#pckgDirType");	// all directories + dirs with '*' 
    public static final URI indirectPckgDirType = createReloURI("jdt#indirectPckgDirType"); // the ones with the '*' on them
    public static final URI pckgDirContains = createReloURI("jdt#pckgDirContains");
    
    // support for a containment cache
    public static final URI containmentBasedDepStrength = createReloURI("jdt#depStrength");
    public static final URI containmentCacheValid = createReloURI("jdt#virtualCacheValid");
    public static final URI containmentBasedCalls = createReloURI("jdt#containmentBasedCalls");
    public static final URI containmentBasedInherits = createReloURI("jdt#containmentBasedInherits");
    public static final URI containmentBasedRefType = createReloURI("jdt#containmentBasedRefType");

	// above properties as directed relationships
    public static final DirectedRel fwdPckgDirContains = DirectedRel.getFwd(pckgDirContains);

    /* Conversion between main types in the Relo-JDT world...
	 * Main participants: 
	 *  Resource (from RDF), 
	 *  IBinding (via ASTNode.resolveBinding() - provided by ASTParser), 
	 *  IJavaElemment (via Eclipse JDT)
	 * 
	 * IBinding -> Resource
	 * IJavaElement -> Resource
	 * Resource -> IJavaElement
	 * Resource -> IBinding/ASTNode [not providing since it doesn't make sense]
	 * IJavaElement <=> IBinding/ASTNode [needed only by the system, so not worrying]
	 */
	
    private interface idSource {
        // returns closes element that can be instantiated 
        public idSource getGroundedSrc();
        public boolean isNull();
        public String getName();
        public boolean isPackage(); 
        public boolean isType(); 
        public boolean isMethod(); 
        public idSource getParent();
        public String getDbgID();
        public Class getDbgWrappedType();
        
        // we do a small optimization by not including the fully qualified name
		// for the param. types and only including the type name
        public String getParameterTypes(String delim);
    };
    
    private static class bindingWrapper implements idSource {
        private final IBinding binding;
        public bindingWrapper(IBinding binding) { this.binding = binding; }
        public boolean isNull() { return (binding==null); }
        public String getName() { return binding.getName(); }
        public boolean isPackage() { return (binding instanceof IPackageBinding); } 
        public boolean isMethod() { return (binding instanceof IMethodBinding); } 
        public boolean isType() { return (binding instanceof ITypeBinding); } 
        public idSource getGroundedSrc() {
            // all bindings that we get should be instantiatable
            return this;
        }
        public idSource getParent() {
            IBinding parentBinding = null;
            if (binding instanceof ITypeBinding) {
                parentBinding = ((ITypeBinding) binding).getDeclaringClass();
                if (parentBinding == null) parentBinding = ((ITypeBinding) binding).getPackage();
            } else if (binding instanceof IMethodBinding)
                parentBinding = ((IMethodBinding) binding).getDeclaringClass();
            else if (binding instanceof IVariableBinding)
                parentBinding = ((IVariableBinding) binding).getDeclaringClass();
            else
                System.err.println("Parent Id requested for unexpected binding type: " + binding.getClass());

            return new bindingWrapper(parentBinding);
        }
        public String getParameterTypes(String delim) {
            String types = "";
            ITypeBinding[] paramTypes = ((IMethodBinding) binding).getParameterTypes();
            for (int i=0; i<paramTypes.length; i++)
                types += paramTypes[i].getName() + delim;
            return types;
        }
        public String getDbgID() {
            return binding.getKey();
        }
        public Class getDbgWrappedType() {
            return binding.getClass();
        }
    };
    
    private static class elementWrapper implements idSource {
        private final IJavaElement element;
        public elementWrapper(IJavaElement element) {
    		IJavaElement ije = element;

    		if (element instanceof IClassFile) {
           		try {
           			ije = ((IClassFile)element).getType();
           		} catch (Exception e) {
           			e.printStackTrace();
           		}
        	}

       		this.element = ije;
        }
        public boolean isNull() { return (element==null); }
        public String getName() {
        	if (element instanceof IType && element.getElementName().length() == 0) {
        		return "" + getParentAnonClassCnt((IType) element);
        	}
            return element.getElementName(); 
        }
        public boolean isPackage() { return (element instanceof IPackageFragment); }
        public boolean isType() { return (element instanceof IType); } 
        public boolean isMethod() { return (element instanceof IMethod); }
        private boolean isClassFile() { return (element instanceof IClassFile); }
        public idSource getGroundedSrc() {
            if (element == null) return null;

            if (element instanceof IType 
                    || element instanceof IMethod
                    || element instanceof IField
                    || element instanceof IPackageFragment )
                return this;
            if(isClassFile()) {
            	return new elementWrapper(((IClassFile)element).getPrimaryElement());
            }

            if (element instanceof ICompilationUnit)
                return new elementWrapper(((ICompilationUnit)element).findPrimaryType());

            return (new elementWrapper(element.getParent())).getGroundedSrc();
        }
        public idSource getParent() {
            IJavaElement parentElement = ((IMember) element).getDeclaringType();
            if (parentElement == null) parentElement = ((IType) element).getPackageFragment();
            return new elementWrapper(parentElement);
        }
        public String getParameterTypes(String delim) {
            String types = "";
            String[] paramTypes = ((IMethod)element).getParameterTypes();
            for (int i=0; i<paramTypes.length; i++) {
                types += Signature.getTypeErasure(Signature.getSignatureSimpleName(paramTypes[i])) + delim;
            }
            return types;
        }
        public String getDbgID() {
            return element.getHandleIdentifier();
        }
        public Class getDbgWrappedType() {
            return element.getClass();
        }
    };
    
    private static String getId(idSource idSrc) {
        try {
            if (!idSrc.isNull()) idSrc = idSrc.getGroundedSrc();
            if (idSrc==null || idSrc.isNull()) return "";
            
            String bindingId = null;
            
            if (idSrc.isPackage())
                bindingId = idSrc.getName();
            else if (idSrc.isType())
                bindingId = getId(idSrc.getParent()) + "$" + idSrc.getName();
            else
                bindingId = getId(idSrc.getParent()) + "." + idSrc.getName();
            
            // add parameters names
            if (idSrc.isMethod())
                bindingId += "(" + idSrc.getParameterTypes(",") + ")";
            
            return bindingId;
        } catch (Throwable t) {
            logger.error("Error while trying to getId " + idSrc.getDbgID() + " type: " + idSrc.getDbgWrappedType(), t);
            return "";
        }
    }
    
    private static IJavaElement getElement(String id) throws JavaModelException {
        int pckgEndSep = id.indexOf("$");
        
        // get package
        String pkgName;
        if (pckgEndSep != -1)
            pkgName = id.substring(0, pckgEndSep);
        else
            pkgName = id;
        IWorkspaceRoot root = ResourcesPlugin.getWorkspace().getRoot();
        IProject[] projects = root.getProjects();
        for (int i=0;i<projects.length; i++) {
            IJavaProject project = null;
            try {
                project = JavaCore.create(projects[i]);
            } catch (IllegalArgumentException iae) {}
            if (project == null || !project.exists()) continue;

            IPackageFragmentRoot[] pfrs = project.getAllPackageFragmentRoots();
            for (int j=0; j<pfrs.length; j++) {
                IPackageFragment pckgFgmt = pfrs[j].getPackageFragment(pkgName);
                if (!pckgFgmt.exists()) continue;

                if (pckgEndSep == -1) {
                    // requesting package
                    //logger.info("Converting: " + id + " --> " + tgtPckgFgmt.getElementName());
                    return pckgFgmt;
                } else {
                    IJavaElement pckgChild = getPackageChildJavaElement(pckgFgmt, id, pckgEndSep);
                    if (pckgChild != null) 
                        return pckgChild;
                    else
                        pckgFgmt = null; // get next tgtPckgFgmt
                }
            }
        }
        

        // not found
        logger.error("Could not find java element for id: " + id);
        return null;
    }
    

   private static IJavaElement getPackageChildJavaElement(IPackageFragment tgtPckgFgmt, String id, int typesBegSep) throws JavaModelException {
        //logger.info("Searching for: " + id + " in: " + tgtPckgFgmt.getElementName());
        String idTypeNames = null;
        IType tgtType = null;
        int typesEndSep = id.indexOf(".", id.lastIndexOf("$"));
        if (typesEndSep == -1)
            idTypeNames = id.substring(typesBegSep);
        else
            idTypeNames = id.substring(typesBegSep, typesEndSep);

        StringTokenizer typeTokenizer = new StringTokenizer(idTypeNames, "$");
        while (typeTokenizer.hasMoreTokens()) {
            String typeName = typeTokenizer.nextToken();
            if (tgtType == null) {
                tgtType = tgtPckgFgmt.getCompilationUnit(typeName + ".java").findPrimaryType();
            } else {
            	// inner classes can be anonymous
                if (Character.isDigit(typeName.charAt(0)))
                	tgtType = getAnonType(tgtType, Integer.parseInt(typeName));
                else
                	tgtType = tgtType.getType(typeName);
            }
        }
		// TODO: Add comment on why this is needed [CyrusK]
        if(tgtType == null) {
      	  tgtType = tgtPckgFgmt.getClassFile(idTypeNames.substring(1) + ".class").getType();
        }

        if (typesEndSep == -1) {
            // logger.info("Converting: " + id + " --> " + tgtPckgFgmt.getElementName() + "//" + tgtType.getElementName());
            return tgtType;
        }
        
        // logger.info("Converting: " + id + " --> " + tgtPckgFgmt.getElementName() + "//" + tgtType.getElementName() + " ...");

        if (tgtType == null)
            logger.error("tgtType is null! Package: " + tgtPckgFgmt + " // type names: " + idTypeNames);

        // get member
        int methSep = id.indexOf("(");
        if (methSep == -1) {
            // field
            return tgtType.getField(id.substring(typesEndSep + 1, id.length()));
        } else {
            // method
            return getMethodJavaElement(tgtType, id, typesEndSep, methSep);
        }

        //return null;
    }

     private static IJavaElement getMethodJavaElement(IType tgtType, String id, int typesEndSep, int methSep) throws JavaModelException {
        //logger.info("Searching for: " + id + " in: " + tgtType.getElementName());
        String methName = id.substring(typesEndSep + 1, methSep);
        IMethod[] meth = tgtType.getMethods();
        for (int i = 0; i < meth.length; i++) {
            if (!meth[i].getElementName().equals(methName)) continue;
            
            String[] paramTypes = meth[i].getParameterTypes();
            int paramCnt = 0;
            StringTokenizer methParamTypesTokenizer = new StringTokenizer(id.substring(methSep), "(),");
            while (methParamTypesTokenizer.hasMoreTokens()) {
                if (paramCnt>=paramTypes.length) break;
                String idTypeName = methParamTypesTokenizer.nextToken();
                String elemTypeName = Signature.getTypeErasure(Signature.getSignatureSimpleName(paramTypes[paramCnt]));
                if (!idTypeName.equals(elemTypeName)) break;
                paramCnt++;
            }
            if (paramCnt == paramTypes.length && !methParamTypesTokenizer.hasMoreTokens()) return meth[i];
        }
        return null;
    }

	/**
	 * Given an anon inner class, this method returns the count that the parent
	 * has for this class.
	 * 
	 * This is done by finding the parent method and the parent class, then
	 * going through all the methods in the parent class, to add up all the
	 * previous anon classes.
	 */
	protected static int getParentAnonClassCnt(IType anonClass) {
		IMember anonClassContainerMember = (IMember) anonClass.getParent();
		IType parentClass = anonClassContainerMember.getDeclaringType();
		//int parentAnonClassCnt = anonClass.getOccurrenceCount();
        //for Eclipse 3.1 compatability
        int parentAnonClassCnt = ((SourceRefElement)anonClass).occurrenceCount;
		try {
			for (IMember parentClassChild : parentClass.getFields()) {
				if (parentClassChild.equals(anonClassContainerMember)) return parentAnonClassCnt;
				for (IJavaElement childsChild : parentClassChild.getChildren()) {
					if (childsChild instanceof IType && ((IType)childsChild).isAnonymous())  {
						parentAnonClassCnt++;
					}
				}
			}
			for (IMember parentClassChild : parentClass.getMethods()) {
				if (parentClassChild.equals(anonClassContainerMember)) return parentAnonClassCnt;
				for (IJavaElement childsChild : parentClassChild.getChildren()) {
					if (childsChild instanceof IType && ((IType)childsChild).isAnonymous())  {
						parentAnonClassCnt++;
					}
				}
			}
		} catch (JavaModelException e) {	logger.error("Unexpected Exception", e);	}
		return parentAnonClassCnt;
	}
	
	/**
	 * Given a parent class and the count associated with it, this method finds
	 * the appropriate anon inner class.
	 * 
	 * This is done by going thorugh all the methods of the given parent class,
	 * and counting the anon classes till we get our count.
	 */
	protected static IType getAnonType(IType parentType, int parentAnonClassCnt) throws JavaModelException {
		for (IMember parentClassChild : parentType.getFields()) {
			for (IJavaElement childsChild : parentClassChild.getChildren()) {
				if (childsChild instanceof IType && ((IType)childsChild).isAnonymous())  {
					parentAnonClassCnt--;
					if (parentAnonClassCnt == 0) return (IType) childsChild;
				}
			}
		}
		for (IMember parentClassChild : parentType.getMethods()) {
			for (IJavaElement childsChild : parentClassChild.getChildren()) {
				if (childsChild instanceof IType && ((IType)childsChild).isAnonymous())  {
					parentAnonClassCnt--;
					if (parentAnonClassCnt == 0) return (IType) childsChild;
				}
			}
		}
    	return null;
	}

    public final static String jdtWkspcNS = "jdt-wkspc#";
	public static Resource bindingToResource(ReloRdfRepository reloRepo, IBinding binding) {
		//logger.info("getId returning: " + getId(new bindingWrapper(binding)) + " <-- " + binding.getKey());
		return ReloCore.idToResource(reloRepo, jdtWkspcNS, getId(new bindingWrapper(binding)));
	}

	public static Resource elementToResource(ReloRdfRepository reloRepo, IJavaElement element) {
        //logger.info("getId returning: " + getId(new elementWrapper(element)) + " <-- " + element.getHandleIdentifier());
		return ReloCore.idToResource(reloRepo, jdtWkspcNS, getId(new elementWrapper(element)));
	}


    /**
	 * Note: returns null for project rdf resources
	 * @param res
	 * @return
	 */
	public static IJavaElement resourceToElement(ReloRdfRepository repo, Resource res) {
        if (!(res instanceof URI)) {
            logger.error("Cannot find element for Resource: " + res + 
                    " [type: " + repo.getStatement(res, repo.rdfType, null).getObject().toString() + "]");
        }
        try {
            return RJCore.getElement(((URI)res).getLocalName());
        } catch (JavaModelException e) {
            logger.error("Cannot find element for Resource: " + res + 
                    " [type: " + repo.getStatement(res, repo.rdfType, null).getObject().toString() + "]", e);
        }
        return null;
	}

}
